/*
 * Copyright (C) 2018 The Asiainfo-Ability Authors
 *
 *      Licensed under the Apache License, Version 2.0 (the "License");
 *      you may not use this file except in compliance with the License.
 *      You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *      Unless required by applicable law or agreed to in writing, software
 *      distributed under the License is distributed on an "AS IS" BASIS,
 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *      See the License for the specific language governing permissions and
 *      limitations under the License.
 */

package org.asiainfo.ability.gateway.service.support;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.asiainfo.ability.gateway.code.OnOffState;
import org.asiainfo.ability.base.code.SystemCode;
import org.asiainfo.ability.base.utils.Response;
import org.asiainfo.ability.gateway.context.AbilityRequestContext;
import org.asiainfo.ability.gateway.custom.FastRateLimiter;
import org.asiainfo.ability.gateway.model.Ability;
import org.asiainfo.ability.gateway.model.User;
import org.asiainfo.ability.gateway.model.UserAbility;
import org.asiainfo.ability.gateway.service.CheckService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * @Author: visen
 * @Date: 2018/2/2
 * @Description:
 */
@Service
public class AbilityCheckService implements CheckService {

    @Autowired
    private AbilityService abilityService;

    @PostConstruct
    public void init() {
        String s = null;
    }

    public static void main(String[] args) {
        CaffeineCache cache = new CaffeineCache("a",
                Caffeine.newBuilder().recordStats()
                        .expireAfterAccess(3, TimeUnit.SECONDS)
                        .maximumSize(1024)
                        .build());
        Cache cache1 = Caffeine.newBuilder()
                .expireAfterAccess(3, TimeUnit.SECONDS)
                .maximumSize(1024)
                .build();

        cache1.put("a", "bbb");
        cache1.put("a", "bbb");

        System.out.println(cache1.getIfPresent("a"));
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cache1.getIfPresent("a"));
    }

    private final static ConcurrentHashMap<String, FastRateLimiter> ABILITY_RATELIMIT
            = new ConcurrentHashMap(8);
    private final static ConcurrentHashMap<String, FastRateLimiter> USER_ABILITY_RATELIMIT
            = new ConcurrentHashMap(32);
    private final static ConcurrentHashMap<String, FastRateLimiter> ABILITY_MAXTIMEOUT_TIMES
            = new ConcurrentHashMap(8);
    private final static ConcurrentHashMap<String, FastRateLimiter> USER_ABILITY_MAXTIMEOUT_TIMES
            = new ConcurrentHashMap(32);

    @Component
    @ConfigurationProperties(prefix = "ability.limit")
    public static class CheckParam {
        private boolean rateLimitEnabled;
        private boolean perRateLimitEnabled;
        private boolean timeoutEnabled;
        private boolean maxTimeoutTimesEnabled;
        private boolean perMaxTimeoutTimesEnabled;

        public boolean getRateLimitEnabled() {
            return rateLimitEnabled;
        }

        public boolean isRateLimitEnabled() {
            return rateLimitEnabled;
        }

        public boolean isPerRateLimitEnabled() {
            return perRateLimitEnabled;
        }

        public void setPerRateLimitEnabled(boolean perRateLimitEnabled) {
            this.perRateLimitEnabled = perRateLimitEnabled;
        }

        public boolean isTimeoutEnabled() {
            return timeoutEnabled;
        }

        public void setTimeoutEnabled(boolean timeoutEnabled) {
            this.timeoutEnabled = timeoutEnabled;
        }

        public boolean isMaxTimeoutTimesEnabled() {
            return maxTimeoutTimesEnabled;
        }

        public void setMaxTimeoutTimesEnabled(boolean maxTimeoutTimesEnabled) {
            this.maxTimeoutTimesEnabled = maxTimeoutTimesEnabled;
        }

        public boolean isPerMaxTimeoutTimesEnabled() {
            return perMaxTimeoutTimesEnabled;
        }

        public void setPerMaxTimeoutTimesEnabled(boolean perMaxTimeoutTimesEnabled) {
            this.perMaxTimeoutTimesEnabled = perMaxTimeoutTimesEnabled;
        }

        public void setRateLimitEnabled(boolean rateLimitEnabled) {
            this.rateLimitEnabled = rateLimitEnabled;
        }

    }

    @Autowired
    private CheckParam checkParam;

    public CheckParam getCheckParam() {
        return checkParam;
    }

    private boolean enabled;

    public String check(AbilityRequestContext context) {

        HttpServletRequest request = context.getRequest();
        String uri = request.getRequestURI(), baseUrl = abilityService.getBaseUrl();
        int ix = uri.indexOf(baseUrl);
        String abilityName = null;
        if (ix == 0) {
            abilityName = uri.substring(baseUrl.length());
        }
        String clientId = request.getHeader("clientId");
        String token = request.getHeader("token");
        //yyyyMMddHHmmssSSS
        String timestamp = request.getHeader("timestamp");

        /*
        if (StringUtils.isBlank(clientId)
                || StringUtils.isBlank(token)
                || StringUtils.isBlank(timestamp)) {
            return Response.error(SystemCode.missing_parameter);
        }
        User user = abilityService.getUser(clientId);
        if (user == null) {
            return Response.error(SystemCode.invalid_client);
        }
        */
        User user = new User();
        user.setClientId("abc");
        Ability ability = abilityService.getAbilityByName(abilityName);
        if (ability == null) {
            return Response.error(SystemCode.invalid_ability);
        } else if (ability.getState() == OnOffState.off) {
            return Response.error(SystemCode.invalid_shutdown);
        }
        UserAbility userAbility = null;
        /*
        if (ability.getMethod() != null && !ability.getMethod().equals(ability.getMethod())) {
            return Response.error(SystemCode.invalid_parameter);
        }
        UserAbility userAbility = abilityService.getUserAbility(user.getClientId(), ability.getName());

        if (user.getUserType() == UserType.normal && ability.getIsPrivate()) {
            if (userAbility == null || userAbility.getState() == OnOffState.off) {
                return Response.error(SystemCode.invalid_permission);
            }
        }
        */
        context.setUserAbility(userAbility);
        context.setAbility(ability);
        context.setUser(user);
        return null;
        //return checkRateLimit(context);
    }


    public String checkRateLimit(AbilityRequestContext context) {

        Ability ability = context.getAbility();
        String error = null;
        if (checkParam.isRateLimitEnabled()) {
            error = checkRateLimit(ABILITY_RATELIMIT, ability.getName(), ability.getRateLimit());
            if (error != null) {
                return error;
            }
        }
        int rateLimit = ability.getPerRateLimit();
        UserAbility userAbility = context.getUserAbility();
        if (userAbility != null) {
            context.setUserAbility(userAbility);
            if (userAbility.getRateLimit() != 0) {
                rateLimit = userAbility.getRateLimit();
            }
        }
        User user = context.getUser();
        if (checkParam.isPerRateLimitEnabled()) {
            error = checkRateLimit(USER_ABILITY_RATELIMIT, user.getClientId() + ability.getName()
                    , rateLimit);
            if (error != null) {
                return error;
            }
        }
        error = checkThreshold(user, ability, userAbility);
        if (error != null) {
            return error;
        }
        return null;
    }

    public String checkThreshold(User user, Ability ability, UserAbility userAbility) {
        if (ability.getMaxTimeoutTimes() > 0) {
            if (checkParam.isMaxTimeoutTimesEnabled()) {
                FastRateLimiter rateLimiter = ABILITY_MAXTIMEOUT_TIMES.get(ability.getName());
                if (rateLimiter != null && !rateLimiter.tryAcquire()) {
                    return Response.error(SystemCode.max_timeout_times);
                }
            }
            if (checkParam.isPerMaxTimeoutTimesEnabled()) {
                int threshold = ability.getPerMaxTimeoutTimes();
                if (userAbility != null && userAbility.getMaxTimeoutTimes() != 0) {
                    threshold = userAbility.getMaxTimeoutTimes();
                }
                if (threshold > 0) {
                    FastRateLimiter rateLimiter = ABILITY_MAXTIMEOUT_TIMES.get(user.getClientId() + ability.getName());
                    if (rateLimiter != null && !rateLimiter.tryAcquire()) {
                        return Response.error(SystemCode.max_timeout_times);
                    }
                }
            }

        }
        return null;
    }

    private String checkRateLimit(ConcurrentHashMap<String
            , FastRateLimiter> abilityRatelimitMap, String key, int ratelimit) {
        if (ratelimit <= 0) {
            return null;
        }
        FastRateLimiter rateLimiter = abilityRatelimitMap.get(key);
        if (rateLimiter == null) {
            rateLimiter = FastRateLimiter.create(ratelimit, 60, TimeUnit.SECONDS);
            FastRateLimiter oldRateLimiter = abilityRatelimitMap.putIfAbsent(key, rateLimiter);
            if (oldRateLimiter != null && oldRateLimiter != rateLimiter) {
                rateLimiter = oldRateLimiter;
            }
        }
        if (!rateLimiter.tryAcquire()) {
            return Response.error(SystemCode.max_ratelimit, ratelimit);
        }
        return null;
    }

    private void setTimeout(ConcurrentHashMap<String
            , FastRateLimiter> map, String key, int limit) {
        if (limit <= 0) {
            return;
        }
        FastRateLimiter rateLimiter = map.get(key);
        if (rateLimiter == null) {
            rateLimiter = FastRateLimiter.create(limit, 60, TimeUnit.SECONDS);
            FastRateLimiter oldRateLimiter = map.putIfAbsent(key, rateLimiter);
            if (oldRateLimiter != rateLimiter) {
                rateLimiter = oldRateLimiter;
            }
        }
        rateLimiter.tryAcquire();
    }

    @Override
    public void setTimeout(AbilityRequestContext requestContext) {
        Ability ability = requestContext.getAbility();
        if (checkParam.isMaxTimeoutTimesEnabled()) {

            setTimeout(ABILITY_MAXTIMEOUT_TIMES, ability.getName()
                    , ability.getMaxTimeoutTimes());
        }
        if (checkParam.isPerMaxTimeoutTimesEnabled()) {
            UserAbility userAbility = requestContext.getUserAbility();
            int threshold = ability.getPerMaxTimeoutTimes();
            if (userAbility != null) {
                if (userAbility.getMaxTimeoutTimes() != 0) {
                    threshold = userAbility.getMaxTimeoutTimes();
                }
            }
            setTimeout(USER_ABILITY_MAXTIMEOUT_TIMES, requestContext.getUser().getClientId() + ability.getName()
                    , threshold);
        }

    }

}
